Skip to content

feat(mobile): add Apple Kilo Pass purchases#3079

Open
iscekic wants to merge 76 commits intomainfrom
feat/apple-iap-personal-credit-purchases
Open

feat(mobile): add Apple Kilo Pass purchases#3079
iscekic wants to merge 76 commits intomainfrom
feat/apple-iap-personal-credit-purchases

Conversation

@iscekic
Copy link
Copy Markdown
Contributor

@iscekic iscekic commented May 6, 2026

Summary

This PR replaces the original one-time Apple credit purchase experiment with App Store-managed Kilo Pass subscriptions across the data model, backend APIs, web management surfaces, and mobile purchase flow.

  • Adds provider-aware Kilo Pass persistence for Stripe vs store-managed subscriptions, including store purchase/event tables, provider subscription IDs, App Store account tokens, monthly App Store product metadata, and Google Play placeholders for the shared catalog.
  • Adds App Store server-side handling for signed transaction verification, idempotent purchase completion, renewals, cancellation state, failed renewals, upgrades/proration, refunds/revocations, credit clawbacks, and mobile-facing Kilo Pass store product/completion router methods.
  • Adds the iOS mobile Kilo Pass subscription experience: native modal presentation, App Store product loading, purchase/recovery hooks, account-token matching, ownership and cancellation states, App Store management routing, and profile dismissal/routing behavior after purchase.
  • Updates web Kilo Pass management so store-managed subscriptions route to App Store management instead of Stripe-only actions, and updates /device-auth to show the signed-in account plus a sign-out path before approving a device.
  • Adds Expo IAP/App Store server dependencies, env placeholders, generated DB migration artifacts, and focused tests around the new store subscription, notification, mobile purchase, and management flows.

Verification

  • App Store sandbox upgrade/downgrade sequence was manually checked against DB rows for subscription, store event, and credit ledger behavior in earlier branch validation.
  • Not manually re-verified during this PR-description update.

Visual Changes

Visual changes exist for the mobile Kilo Pass/profile purchase flow and web account-management surfaces, but screenshots are not attached in this description update.

Reviewer Notes

  • App Store behavior depends on production/sandbox config: root certificates, private key, key ID, issuer ID, Apple app ID, bundle ID, product IDs, subscription group, sandbox testers, and APPLE_IAP_ENVIRONMENT.
  • The current store catalog is monthly-only for App Store. Google Play product/base-plan metadata is present as a placeholder, but Google Play verification and notifications are not implemented here.
  • Store-managed subscriptions intentionally bypass Stripe portal, cancel, schedule, payment-method, and billing-history paths; please check those provider guards preserve existing Stripe behavior.
  • Purchase completion finishes StoreKit transactions only after backend completion so failed completions can be recovered on app start.
  • Refund/revocation handling reverses App Store base and bonus credits as negative ledger entries and depends on usable transaction price data from Apple.

iscekic added 30 commits May 6, 2026 14:36
iscekic added 26 commits May 7, 2026 18:13
…al-credit-purchases

# Conflicts:
#	packages/db/src/migrations/meta/0115_snapshot.json
#	packages/db/src/migrations/meta/0116_snapshot.json
#	packages/db/src/migrations/meta/0117_snapshot.json
#	packages/db/src/migrations/meta/_journal.json
#	pnpm-lock.yaml
@iscekic iscekic marked this pull request as ready for review May 8, 2026 18:49
…al-credit-purchases

# Conflicts:
#	apps/web/package.json
#	apps/web/src/routers/kilo-pass-router.ts
#	packages/db/src/migrations/meta/0123_snapshot.json
#	packages/db/src/migrations/meta/_journal.json
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
try {
await showManageSubscriptionsIOS();
await invalidateKiloPassState();
setTimeout(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Uncleared setTimeout causes a potential memory leak / stale state update

If the component unmounts within 2 seconds of the user returning from the App Store sheet, invalidateKiloPassState() will fire on an unmounted component. This is a classic leak pattern in React Native.

Consider using a ref to track mount state, or simply rely on the useEffect invalidation in useStoreKiloPassPurchase / react-query's own window-focus refetch instead of a delayed re-check:

Suggested change
setTimeout(() => {
void invalidateKiloPassState();

showError: (message: string) => void;
};

const sharedPurchaseCompletions = new Map<string, Promise<boolean>>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Module-level mutable state is a potential memory leak in React Native

sharedPurchaseCompletions and lastPurchaseErrorToast are module-level singletons that accumulate state across the app's lifetime and are never cleaned up. In React Native's Metro bundler, modules persist across hot reloads (Fast Refresh), so test/dev scenarios can see stale entries. More critically, sharedPurchaseCompletions holds Promise<boolean> references that may keep closures alive.

This is intentional cross-instance deduplication, but consider documenting this explicitly and ensuring entries are always deleted after completion (they are, via .delete(purchaseId)), and confirm no error paths skip the delete.

@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot Bot commented May 8, 2026

Code Review Summary

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/web/src/lib/user.ts 810–835 GDPR softDeleteUser not updated for new app_store_account_token column and new kilo_pass_store_purchases / kilo_pass_store_events tables — see AGENTS.md rule

WARNING

File Line Issue
apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.tsx 81 Uncleared setTimeout fires invalidateKiloPassState on a potentially unmounted component
apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts 42–43 Module-level mutable sharedPurchaseCompletions and lastPurchaseErrorToast — state persists across Fast Refresh reloads; lastPurchaseErrorToast is never reset on sign-out

SUGGESTION

File Line Issue
apps/web/src/lib/kilo-pass/state.ts 149 stripeSubscriptionId fallback silently coerces App Store providerSubscriptionId into a Stripe field, masking the conceptual mismatch
Other Observations (not in diff)

CRITICAL — GDPR soft-delete gap (apps/web/src/lib/user.ts:810)

The AGENTS.md rules explicitly require: "When adding PII … to the database — whether as a new table or a new column — you MUST also update the GDPR soft-delete flow in softDeleteUser (apps/web/src/lib/user.ts)".

This PR adds:

  1. kilocode_users.app_store_account_token — a UUID unique to each user that directly links them to their App Store purchase history. The softDeleteUser anonymization block (lines 810–835) does not rotate or nullify this token, leaving the deleted user's App Store identity traceable after soft-delete.
  2. kilo_pass_store_purchases — contains app_account_token, purchase_token (JWS), provider_transaction_id, provider_subscription_id, and raw_payload_json per user. None of this is deleted or anonymized by softDeleteUser.
  3. kilo_pass_store_events — also linked by app_account_token and holds payload JSON.

Recommendation:

  • In the softDeleteUser anonymization block, rotate app_store_account_token to a new crypto.randomUUID() so the token no longer maps to the original account.
  • Decide whether kilo_pass_store_purchases / kilo_pass_store_events rows should be hard-deleted or anonymized (the app_account_token columns are FK-less and retain the old UUID even after the user row is rotated).
  • Add a test in apps/web/src/lib/user.test.ts per the spec.

SUGGESTION — stripeSubscriptionId field overloading (apps/web/src/lib/kilo-pass/state.ts:149)

stripeSubscriptionId: selected.stripeSubscriptionId ?? selected.providerSubscriptionId ?? '',

This silently reuses a Stripe-named field to carry an Apple originalTransactionId for App Store subscriptions. All downstream callers that actually use this value for Stripe API calls are now gated behind assertStripeManagedSubscription, so there's no runtime error today. But the field name is misleading and could trip up future callers. Consider either (a) documenting the intent with a comment, or (b) keeping stripeSubscriptionId as string | null in the public type and letting callers assert non-null only when they've already confirmed Stripe ownership.

Files Reviewed (56 files)
  • .env.local.example
  • apps/mobile/AGENTS.md
  • apps/mobile/README.md
  • apps/mobile/app.config.ts
  • apps/mobile/metro.config.js
  • apps/mobile/package.json
  • apps/mobile/src/app/(app)/(tabs)/(1_kiloclaw)/index.tsx
  • apps/mobile/src/app/(app)/(tabs)/_layout.tsx
  • apps/mobile/src/app/(app)/_layout.tsx
  • apps/mobile/src/app/(app)/kilo-pass.tsx
  • apps/mobile/src/components/agents/session-list-content.tsx
  • apps/mobile/src/components/agents/session-list-routes.ts
  • apps/mobile/src/components/agents/session-list-screen.tsx
  • apps/mobile/src/components/home/greeting.tsx
  • apps/mobile/src/components/home/home-screen.tsx
  • apps/mobile/src/components/kilo-chat/conversation-screen.tsx
  • apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.tsx — 1 issue
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-screen.tsx
  • apps/mobile/src/components/profile-avatar-button.tsx
  • apps/mobile/src/components/profile-credits-card.tsx
  • apps/mobile/src/components/profile-screen.tsx
  • apps/mobile/src/lib/auth/auth-context.tsx
  • apps/mobile/src/lib/auth/trpc-unauthorized.ts
  • apps/mobile/src/lib/kilo-pass/dev-storekit-refund.ts
  • apps/mobile/src/lib/kilo-pass/navigation.ts
  • apps/mobile/src/lib/kilo-pass/store-ownership.ts
  • apps/mobile/src/lib/kilo-pass/store-products-loader.ts
  • apps/mobile/src/lib/kilo-pass/store-products.ts
  • apps/mobile/src/lib/kilo-pass/subscription-card-state.ts
  • apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-products.ts
  • apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts — 1 issue
  • apps/mobile/src/lib/query-client.ts
  • apps/mobile/src/lib/tab-bar-layout.ts
  • apps/web/package.json
  • apps/web/src/app/api/kilo-pass/apple/notifications/route.ts
  • apps/web/src/app/device-auth/DeviceAuthClient.tsx
  • apps/web/src/app/device-auth/page.tsx
  • apps/web/src/components/profile/kilo-pass/KiloPassActiveSubscriptionCard.tsx
  • apps/web/src/components/profile/kilo-pass/kiloPassManagementAction.ts
  • apps/web/src/components/subscriptions/kilo-pass/KiloPassDetail.logic.ts
  • apps/web/src/components/subscriptions/kilo-pass/KiloPassDetail.tsx
  • apps/web/src/lib/kilo-pass/apple-store-notifications.ts
  • apps/web/src/lib/kilo-pass/apple-store-sdk.ts
  • apps/web/src/lib/kilo-pass/apple-store-verifier.ts
  • apps/web/src/lib/kilo-pass/enums.ts
  • apps/web/src/lib/kilo-pass/issuance.ts
  • apps/web/src/lib/kilo-pass/mobile-store-products.ts
  • apps/web/src/lib/kilo-pass/state.ts — 1 suggestion
  • apps/web/src/lib/kilo-pass/store-subscription-completion.ts
  • apps/web/src/lib/kilo-pass/stripe-handlers-invoice-paid.ts
  • apps/web/src/lib/kilo-pass/stripe-handlers-subscription-events.ts
  • apps/web/src/lib/kilo-pass/yearly-monthly-base-cron.ts
  • apps/web/src/routers/kilo-pass-router.ts
  • packages/db/src/migrations/0123_tricky_agent_brand.sql
  • apps/web/src/lib/user.ts (unchanged — GDPR gap flagged)

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-4.6 · 7,349,074 tokens

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant